<?php

// ------------------------------------------------------------------------
// |@Author       : Jarmin <edshop@qq.com>
// |@----------------------------------------------------------------------
// |@Date         : 2023-06-05 15:17:15
// |@----------------------------------------------------------------------
// |@LastEditTime : 2025-05-16 19:32:33
// |@----------------------------------------------------------------------
// |@LastEditors  : Jarmin <topextend@qq.com>
// |@----------------------------------------------------------------------
// |@Description  : 随机数码管理扩展（提供加密安全的数据处理功能）
// |@----------------------------------------------------------------------
// |@FilePath     : CodeExtend.php
// |@----------------------------------------------------------------------
// |@Copyright (c) 2023 http://www.ladmin.cn   All rights reserved.
// ------------------------------------------------------------------------
declare (strict_types=1);

namespace think\admin\extend;

/**
 * 随机数码管理扩展
 *
 * 提供企业级加密安全的数据处理功能，包括：
 * - 符合RFC 4122标准的UUIDv4生成
 * - AES-256-CBC数据加解密
 * - 抗暴力破解的密码哈希
 * - 安全随机字符串生成
 * - 数据压缩/解压
 * @package think\admin\extend
 */
class CodeExtend
{
    /**
     * 生成符合UUIDv4标准的唯一标识
     * @return string 格式：8-4-4-4-12的十六进制字符串（大写）
     */
    public static function uuid(): string
    {
        // 生成16字节密码学安全随机数（满足UUIDv4的熵要求）
        $bytes = random_bytes(16);
        // 设置版本位：第7字节的高4位设为0100（0x40），标记为UUIDv4
        $bytes[6] = chr(ord($bytes[6]) & 0x0f | 0x40);
        // 设置变体位：第9字节的高2位设为10（0x80），符合RFC 4122规范
        $bytes[8] = chr(ord($bytes[8]) & 0x3f | 0x80);
        // 将二进制转换为十六进制并按UUID格式分割
        return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($bytes), 4));
    }

    /**
     * 生成密码学安全随机字符串
     * @param int $size 字符串长度（4-1024）
     * @param int $type 字符类型（1=纯数字，2=纯小写字母，3=数字+字母，4=纯大写字母，5=数字+大写字母，6=大小写字母混合，7=数字+大小写字母混合）
     * @param string $prefix 固定前缀（不计入长度）
     * @return string 生成的随机字符串
     * @throws HttpResponseException
     */
    public static function random(int $size = 10, int $type = 1, string $prefix = ''): string
    {
        // 根据类型选择字符集
        $numbs    = '0123456789';
        $lowerChars = 'abcdefghijklmnopqrstuvwxyz';
        $upperChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';

        $chars = match ($type) {
            1 => $numbs,       // 纯数字
            2 => $lowerChars, // 纯小写字母
            3 => $numbs . $lowerChars, // 数字 + 小写字母
            4 => $upperChars, // 纯大写字母
            5 => $numbs . $upperChars, // 数字 + 大写字母
            6 => $lowerChars . $upperChars, // 大小写字母混合
            7 => $numbs . $lowerChars . $upperChars, // 数字 + 大小写字母混合
            default => $numbs . $lowerChars, // 默认数字+小写，或可抛出异常
        };

        // 生成每个字符时使用密码学安全随机数
        $maxIndex = strlen($chars) - 1;
        $code = $prefix;
        for ($i = 0; $i < $size; $i++) {
            // 使用random_int而非mt_rand以保证安全性
            $code .= $chars[random_int(0, $maxIndex)];
        }
        return $code;
    }

    /**
     * 生成带日期前缀的编码
     * @param int $size 总长度（最小14位）
     * @param string $prefix 自定义前缀（默认空）
     * @return string 格式：YYYYMMDDHHmmss + 随机数字
     */
    public static function uniqidDate(int $size = 16, string $prefix = ''): string
    {
        // 确保最小长度包含完整日期（14位年月日时分秒）
        if ($size < 14) {
            $size = 14;
        }
        // 生成基础编码（日期+前缀）
        $code = $prefix . date('YmdHis');
        // 填充随机数字直到达到指定长度
        while (strlen($code) < $size) {
            $code .= random_int(0, 9); // 使用安全随机数生成器
        }
        return $code;
    }

    /**
     * AES-256-CBC 数据加密
     * @param mixed $data 可序列化的数据
     * @param string $skey 加密密钥（必须32字节）
     * @return string Base64Url编码的加密结果
     * @throws HttpResponseException
     */
    public static function encrypt($data, string $skey): string
    {
        // AES-256-CBC expects a 32-byte key
        if (mb_strlen($skey, '8bit') !== 32) {
            throw new \InvalidArgumentException('AES-256-CBC 加密密钥必须为32字节长。');
        }
        // 生成随机初始化向量（每次加密不同，防止重放攻击）
        $iv = random_bytes(16);
        // 序列化数据以适应复杂数据结构
        $value = openssl_encrypt(serialize($data), 'AES-256-CBC', $skey, 0, $iv);

        // 组合加密要素并进行安全编码
        return static::enSafe64(json_encode([
            'iv' => base64_encode($iv),  // 存储Base64编码的IV
            'value' => $value            // 加密后的二进制数据
        ]));
    }

    /**
     * AES-256-CBC 数据解密
     * @param string $data Base64Url编码的加密数据
     * @param string $skey 加密密钥
     * @return mixed 原始数据
     * @throws HttpResponseException
     */
    public static function decrypt(string $data, string $skey)
    {
        // AES-256-CBC expects a 32-byte key
        if (mb_strlen($skey, '8bit') !== 32) {
            throw new \InvalidArgumentException('AES-256-CBC 解密密钥必须为32字节长。');
        }
        // 解码并验证数据结构完整性
        $json = static::deSafe64($data);
        $attr = json_decode($json, true);

        // 还原初始化向量（IV）并进行解密操作
        $iv = base64_decode($attr['iv']);
        $decrypted = openssl_decrypt($attr['value'], 'AES-256-CBC', $skey, 0, $iv);

        // 反序列化原始数据
        return unserialize($decrypted);
    }

    /**
     * 使用Argon2id算法加密密码
     * @param string $password 明文密码
     * @param string $salt 加密盐值
     * @return string 加密后的哈希值
     */
    public static function encryptPassword(string $password, string $salt): string
    {
        // 使用固定的盐值进行预处理
        $preparedPassword = hash('sha256', $password . $salt);
        // 使用 password_hash 生成最终的哈希值
        return password_hash($preparedPassword, PASSWORD_BCRYPT, ['cost' => 12]);
    }

    /**
     * 验证密码哈希
     * @param string $password 待验证密码
     * @param string $salt 注册时使用的盐值
     * @param string $hash 存储的哈希值
     * @return bool 验证结果
     */
    public static function verifyPassword(string $password, string $salt, string $hash): bool
    {
        // 使用相同的预处理方式
        $preparedPassword = hash('sha256', $password . $salt);
        // 验证密码
        return password_verify($preparedPassword, $hash);
    }

    /**
     * 生成加密安全码
     * @return string 40字符的十六进制字符串（160位熵）
     */
    public static function generateCode(int $length = 20): string
    {
        // 生成20字节随机数（160位安全强度）
        return bin2hex(random_bytes(($length / 2)));
    }

    /**
     * Base64Url安全编码
     * @param string $text 原始字符串
     * @return string 编码后的字符串
     */
    public static function enSafe64(string $text): string
    {
        // URL安全编码：替换+/为-_，去除填充等号
        return rtrim(strtr(base64_encode($text), '+/', '-_'), '=');
    }

    /**
     * Base64Url安全解码
     * @param string $text 编码后的字符串
     * @return string 解码后的原始字符串
     */
    public static function deSafe64(string $text): string
    {
        // 恢复标准base64字符集
        $text = strtr($text, '-_', '+/');
        // 计算填充长度（base64需要4字节对齐）
        $padLength = ((4 - (strlen($text) % 4)) % 4);
        // 添加填充后解码
        return base64_decode($text . str_repeat('=', $padLength));
    }

    /**
     * 安全压缩数据（带错误处理）
     * @param mixed $data 可序列化数据
     * @return string 压缩后的Base64Url字符串
     * @throws HttpResponseException
     */
    public static function enzip($data): string
    {
        // 使用最大压缩级别（9）压缩序列化数据
        $compressed = gzcompress(serialize($data), 9);
        // 返回安全编码后的字符串
        return static::enSafe64($compressed);
    }

    /**
     * 安全解压数据（带验证）
     * @param string $string 压缩后的字符串
     * @return mixed 原始数据
     * @throws HttpResponseException
     */
    public static function dezip(string $string)
    {
        // 解压缩并验证数据完整性
        $data = gzuncompress(static::deSafe64($string));
        // 反序列化原始数据
        return unserialize($data);
    }

    /**
     * 生成随机的十六进制颜色码
     * @return string 例如 "#A1B2C3"
     */
    public static function getRandomColor(): string
    {
        return sprintf('#%02X%02X%02X', random_int(0, 255), random_int(0, 255), random_int(0, 255));
    }

    /**
     * 生成指定范围内的随机角度
     * @param int $start 最小角度 (默认 -45)
     * @param int $end 最大角度 (默认 45)
     * @return int
     */
    public static function getRandomAngle(int $start = -45, int $end = 45): int
    {
        //确保 min <= max
        if ($start > $end) {
            [$start, $end] = [$end, $start];
        }
        return random_int($start, $end);
    }

    /**
     * 生成指定数量的不重复随机常用汉字 (GB2312 范围，转换为 UTF-8)
     * 依赖 iconv 扩展。
     * @param int $num 生成汉字的数量
     * @return array 包含随机汉字的数组
     * @throws \RuntimeException 如果 iconv 不可用或转换失败
     */
    public static function getRandomChars(int $num): array
    {
        if ($num <= 0) {
            return [];
        }
        $chars = [];
        $attempts = 0; // 防止无限循环
        while (count($chars) < $num && $attempts < ($num * 5)) {
            // GB2312 常用汉字范围 (一级汉字 3755个, 区位码 16-55, 位码 01-94)
            // 为简单起见，这里使用原作者的范围，但可以更精确地映射到常用汉字区
            $gbHigh = random_int(0xB0, 0xD0); // 区码
            $gbLow  = random_int(0xA1, 0xF0); // 位码

            $gbChar = chr($gbHigh) . chr($gbLow);

            if (!function_exists('iconv')) {
                throw new \RuntimeException('iconv 函数不可用，无法生成随机汉字。');
            }

            $utf8Char = @iconv('GB2312', 'UTF-8', $gbChar);

            if ($utf8Char !== false && !in_array($utf8Char, $chars)) {
                $chars[] = $utf8Char;
            }
            $attempts++;
        }
        return $chars;
    }

    /**
     * 计算数组的平均值
     * @param array $array 数值数组
     * @return int 返回四舍五入后的整数平均值。如果数组为空则返回0。
     */
    public static function avg(array $array): int
    {
        if (empty($array)) {
            return 0;
        }
        return intval(round(array_sum($array) / count($array)));
    }

    /**
     * 从数组中随机获取一个元素
     * @param array $array 目标数组
     * @return mixed 如果数组为空则返回 null，否则返回随机选取的元素
     */
    public static function getRandomElement(array $array)
    {
        if (empty($array)) {
            return null;
        }
        return $array[array_rand($array)];
    }
}
